﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.TeamFoundation;
using Microsoft.TeamFoundation.Framework.Server;
using Microsoft.TeamFoundation.WorkItemTracking.Client;

namespace TfsRollUp.Core
{
    /// <summary>
    /// Caches per thread workitem stores, manages <see cref="Settings"/>, and does the actual workitem processing
    /// </summary>
    internal class Service : ITeamFoundationService, IDisposable
    {
        /// <summary>
        /// A thread specific <see cref="ThreadLocal&lt;T&gt;"/> store for tfs collection uri as key and Store's as values
        /// </summary>
        private ThreadLocal<Dictionary<Uri, Store>> _store = new ThreadLocal<Dictionary<Uri, Store>>(() => new Dictionary<Uri, Store>());

        /// <summary>
        /// Tfs context, see <see cref="TeamFoundationRequestContext"/>
        /// </summary>
        private TeamFoundationRequestContext _requestContext;

        /// <summary>
        /// Wcf plugin
        /// </summary>
        private Receiver _receiver;

        /// <summary>
        /// Dead or alive
        /// </summary>
        private bool _disposed;

        /// <summary>
        /// Global settings to this service
        /// </summary>
        internal Settings Settings { get; private set; }

        /// <summary>
        /// The tfs system account sid, used to guard against circular updates
        /// </summary>
        internal string SystemAccountSid { get; private set; }

        /// <summary>
        /// A queue with workitem roots that is to be processed, see <see cref="BlockingCollection&lt;T&gt;"/>
        /// </summary>
        internal BlockingCollection<Tuple<Uri,int>> WorkItemRootQueue { get; private set; }

        /// <summary>
        /// Take all processing requests and have them convoyed into the WorkItemRootQueue
        /// </summary>
        private void PreProcessWorkItem()
        {
            TeamFoundationTrace.MethodEntry();
            var workItemsInProcessing = new ConcurrentDictionary<Tuple<Uri,int>, DateTime>();

            // Block while waiting for new workitems put in the queue from ProcessWorkItem
            foreach (var workItemTuple in WorkItemRootQueue.GetConsumingEnumerable())
            {
                var workItemId = workItemTuple.Item2;
                if (workItemsInProcessing.TryAdd(workItemTuple, DateTime.UtcNow))
                {
                    TeamFoundationTrace.Verbose("{0}.PreProcessWorkItem: WorkItem:{1} added as inprocessing", Constants.TfsRollUpPlugin, workItemId);
                    var currentTuple = workItemTuple;
                    Task.Factory.StartNew(() =>
                    {
                        DateTime now;
                        try
                        {
                            ProcessWorkItem(currentTuple);
                        }
                        catch (Exception ex)
                        {
                            TeamFoundationTrace.TraceException(ex);
                        }
                        finally
                        {
                            if (workItemsInProcessing.TryRemove(currentTuple, out now))
                            {
                                var duration = DateTime.UtcNow - now;
                                TeamFoundationTrace.Verbose("{0}.PreProcessWorkItem: WorkItem:{1} removed from inprocessing list. Processing took {2} ms", Constants.TfsRollUpPlugin, currentTuple.Item2, duration.TotalMilliseconds);
                            }
                            else
                            {
                                TeamFoundationTrace.Error("{0}.PreProcessWorkItem: workitem:{1} should have been found in processing queue, but was not", Constants.TfsRollUpPlugin, currentTuple.Item2);
                            }
                        }
                    });
                }
                else
                {
                    TeamFoundationTrace.Verbose("{0}.PreProcessWorkItem: WorkItemId:{1} already in processing", Constants.TfsRollUpPlugin, workItemId);
                }
            }
            TeamFoundationTrace.MethodExit();
        }

        /// <summary>
        /// Read, calculate and save this workitem, including its children
        /// </summary>
        /// <param name="workItemTuple">The Tfs collection uri, and the workitem id as a <see cref="Tuple"/></param>
        private void ProcessWorkItem(Tuple<Uri,int> workItemTuple)
        {
            TeamFoundationTrace.MethodEntry();
            var uri = workItemTuple.Item1;
            var workItemId = workItemTuple.Item2;

            TeamFoundationTrace.Info("{0}.ProcessWorkItem: Finally, at last, something to do: processing WorkItem:{1}", Constants.TfsRollUpPlugin, workItemId);

            var store = WorkItemStore(uri);
            var workItem = store.GetWorkItem(workItemId);
            var fieldCollection = FieldCollection(uri);
            if (fieldCollection == null)
            {
                TeamFoundationTrace.Error("{0}.ProcessWorkItem: unexpected null for fieldCollection for uri:{1}", Constants.TfsRollUpPlugin, uri);
                TeamFoundationTrace.MethodExit();
                return;
            }

            var fieldList = fieldCollection.GetRollUpFieldList(workItem.Fields[CoreField.TeamProject].Value.ToString(), workItem.Type.Name);
            if (fieldList == null || fieldList.Length == 0)
            {
                TeamFoundationTrace.Verbose("{0}.ProcessWorkItem: RollUp fields for WorkItemId:{1} was not found in TfsCollection:'{2}' TeamProject:'{3}' WorkItemType:'{4}'", Constants.TfsRollUpPlugin, workItem.Id, workItem.Store.TeamProjectCollection.Name, (string)workItem.Fields[CoreField.TeamProject].Value, workItem.Type.Name);
                TeamFoundationTrace.MethodExit();
                return;
            }

            var tree = workItem.CalculateTree(fieldList, Settings);
            if (tree != null && tree.Count > 0)
            {
                var batchSaveErrors = workItem.SaveTree(tree);
                if (batchSaveErrors != null && batchSaveErrors.Length > 0)
                {
                    TeamFoundationTrace.Error("{0}.ProcessWorkItem: BatchSave failed. {1} errors occurred. Starting recalculation", Constants.TfsRollUpPlugin, batchSaveErrors.Length);
                    SendErrorsToTrace(batchSaveErrors);
                    tree = workItem.CalculateTree(fieldList, Settings);
                    batchSaveErrors = workItem.SaveTree(tree);
                    if (batchSaveErrors != null && batchSaveErrors.Length > 0)
                    {
                        TeamFoundationTrace.Error("{0}.ProcessWorkItem: Retry BatchSave failed. {1} errors occurred", Constants.TfsRollUpPlugin, batchSaveErrors.Length);
                        SendErrorsToTrace(batchSaveErrors);
                    }
                }
            }
            TeamFoundationTrace.MethodEntry();
        }

        /// <summary>
        /// Tell about the errors found when trying to save the workitems
        /// </summary>
        /// <param name="batchSaveErrors">A collection of save errors</param>
        private void SendErrorsToTrace(BatchSaveError[] batchSaveErrors)
        {
            foreach (var error in batchSaveErrors)
            {
                TeamFoundationTrace.Error("{0}.SendErrorsToTrace: WorkItem:{1} Exception:{2}", Constants.TfsRollUpPlugin, error.WorkItem.Id, error.Exception);
            }
        }

        /// <summary>
        /// The WorkItemStore, one store is saved per uri per thread <see cref="WorkItemRootQueue"/>
        /// </summary>
        internal WorkItemStore WorkItemStore(Uri uri)
        {
            if (!_store.Value.ContainsKey(uri))
            {
                _store.Value.Add(uri, new Store(uri));
            }
            return _store.Value[uri].WorkItemStore;
        }

        /// <summary>
        /// The WorkItemStore, one store is saved per uri per thread <see cref="WorkItemRootQueue"/>
        /// </summary>
        internal WorkItemStore WorkItemStore(TeamFoundationRequestContext requestContext)
        {
            var uri = Core.Store.GetUriFromRequestContext(requestContext);
            return WorkItemStore(uri);
        }

        /// <summary>
        /// Returns the workitem field collection
        /// </summary>
        /// <param name="uri">The key</param>
        /// <returns>The collection of Alm fields</returns>
        internal WorkItemFieldCollection FieldCollection(Uri uri)
        {
            if (!_store.Value.ContainsKey(uri))
            {
                _store.Value.Add(uri, new Store(uri));
            }
            return _store.Value[uri].WorkItemFieldCollection;
        }

        /// <summary>
        /// Returns the workitem field collection
        /// </summary>
        /// <param name="requestContext">Tfs context <see cref="TeamFoundationRequestContext"/></param>
        /// <returns>The collection of Alm fields</returns>
        internal WorkItemFieldCollection FieldCollection(TeamFoundationRequestContext requestContext)
        {
            var uri = Core.Store.GetUriFromRequestContext(requestContext);
            return FieldCollection(uri);
        }

        #region ITeamFoundationService
        /// <summary>
        /// Stop service
        /// </summary>
        /// <param name="systemRequestContext">Tfs context <see cref="TeamFoundationRequestContext"/></param>
        public void ServiceEnd(TeamFoundationRequestContext systemRequestContext)
        {
            TeamFoundationTrace.MethodEntry(systemRequestContext);
            _store = null;
            _receiver.ServiceOff();
            _receiver = null;
            TeamFoundationTrace.MethodExit();
        }

        /// <summary>
        /// Flush cached objects
        /// </summary>
        public void ServiceFlushCache()
        {
            TeamFoundationTrace.MethodEntry();
            _store = new ThreadLocal<Dictionary<Uri, Store>>(() => new Dictionary<Uri, Store>());
            TeamFoundationTrace.MethodExit();
        }

        /// <summary>
        /// Initialize the service
        /// </summary>
        /// <param name="systemRequestContext">Tfs context</param>
        public void ServiceStart(TeamFoundationRequestContext systemRequestContext)
        {
            TeamFoundationTrace.MethodEntry(systemRequestContext);
            _requestContext = systemRequestContext;
            Settings = Settings.FromString(systemRequestContext);
            _receiver = new Receiver(this);
            if (_receiver.ServiceOn())
            {
                TeamFoundationTrace.Info("{0}.ServiceStart: The Receiver is turned on", Constants.TfsRollUpPlugin);
            }
            SystemAccountSid = _requestContext.UserContext.Identifier;

            // Use this collection to arrange requests into  a convoy
            WorkItemRootQueue = new BlockingCollection<Tuple<Uri, int>>();
            Task.Factory.StartNew(() => { PreProcessWorkItem(); });
            TeamFoundationTrace.MethodExit();
        }
        #endregion ITeamFoundationService

        #region IDisposable

        /// <summary>
        /// Kill
        /// </summary>
        /// <param name="disposing">Are we disposing</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (_receiver != null)
                {
                    ((IDisposable)_receiver).Dispose();
                    _receiver = null;
                }
                if (_store != null)
                {
                    _store.Dispose();
                }

                // Set the sentinel.
                _disposed = true;

                // Suppress finalization of this disposed instance.
                if (disposing)
                {
                    GC.SuppressFinalize(this);
                }
            }
        }

        /// <summary>
        /// Prepare kill
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
        }

        /// <summary>
        /// PrePreKill
        /// </summary>
        ~Service()
        {
            Dispose(false);
        }
        #endregion
    }
}
